package com.hmdp.utils;

import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.hmdp.entity.Shop;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;
import java.util.Objects;
import java.util.concurrent.*;
import java.util.function.Function;

/**
 * @author TJS
 * @Description
 * @create 2023/1/2 13:38
 */
@Slf4j
@Component
public class RedisUtils {

    @Autowired
    private StringRedisTemplate redisTemplate;

    //使用ThreadPoolExecutor
    private static final ExecutorService executorService = new ThreadPoolExecutor(10,10,1,TimeUnit.MINUTES,new ArrayBlockingQueue<>(1024));

    //将任意Java对象序列化为json并存储在string类型的key中，并且可以设置TTL过期时间
    public <T> void set(String key, T value, Long ttl, TimeUnit unit){
        redisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value),ttl,unit);
    }

    //将任意java对象序列化为json并存储在string类型的key中，并且可以设置逻辑过期时间，用于处理缓存击穿问题
    public <T> void setWithLogicalExpire(String key, T value, Long ttl, TimeUnit unit){
        RedisData<T> redisData = new RedisData<>();
        redisData.setData(value);
        redisData.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(ttl)));
        log.info("新的过期时间:{}",redisData.getExpireTime());
        redisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));
    }

    //根据指定的key查询缓存，并反序列化为指定类型，利用缓存空值的方式解决缓存穿透问题
    public  <R,ID extends Number> R queryWithPassThrough(String prex, ID id, Class<R> type, Function<ID,R> dbFallBack, Long ttl, TimeUnit unit) {
        //先查询缓存是否有数据
        String JsonData = redisTemplate.opsForValue().get(prex + id);
        if(StrUtil.isNotBlank(JsonData)){
            /*
                isNotBlank：null ""  " \t\n" 均是false
             */
            //缓存中存在
            return JSONUtil.toBean(JsonData, type);
        }
        if(!Objects.isNull(JsonData)){
            //shopJson 为 "" 或 " \t\n"
            return null;
        }
        //缓存中不存在
        R r = dbFallBack.apply(id);
        if(Objects.isNull(r)){
            //数据库中不存在 ===》 缓存空对象
            set(prex+id,"",ttl,unit);
            return null;
        }
        //数据库中存在
        String jsonStr = JSONUtil.toJsonStr(r);
        //存入缓存
        set(prex + id,jsonStr,ttl,unit);
        //返回
        return r;
    }

    //根据指定的key查询缓存，并反序列化为指定类型，需要利用逻辑过期解决缓存击穿问题
    public  <R,ID extends Number> R queryWithLogicalExpire(String prex, ID id, Class<R> type, Function<ID,R> dbFallBack, Long ttl, TimeUnit unit) {
        //先查询缓存是否有数据
        String jsonData = redisTemplate.opsForValue().get(prex + id);
        if(StrUtil.isBlank(jsonData)){
            //缓存中不存在
            return null;
        }
        //缓存中存在
        JSONObject jsonObject = JSONUtil.parseObj(jsonData);
        R r = jsonObject.get("data", type);
        LocalDateTime expireTime = jsonObject.get("expireTime", LocalDateTime.class);
        //判断是否过期
        if(expireTime.isAfter(LocalDateTime.now())){
            //没有过期，直接返回
            return r;
        }
        //过期了，开启一个线程重建缓存
        //①获取锁
        String lockKey = "lock:shop:"+id;
        boolean tryLock = tryLock(lockKey);
        if(tryLock){
            //②获取锁成功，再次检测redis缓存是否过期，做DoubleCheck,如果存在无需重建缓存
            jsonData = redisTemplate.opsForValue().get("shop:cache:" + id);
            jsonObject = JSONUtil.parseObj(jsonData);
            r = jsonObject.get("data", type);
            expireTime = jsonObject.get("expireTime", LocalDateTime.class);
            if(expireTime.isAfter(LocalDateTime.now())){
                return r;//没有过期，直接返回
            }
            // 开启一个线程重建缓存
            executorService.execute(()->{
                try {
                    /*
                    重建缓存
                    1、查数据库
                    2、存入缓存
                    * */
                    R result = dbFallBack.apply(id);
                    log.info("线程开始执行...");
                    setWithLogicalExpire("shop:cache:"+id,result,ttl,unit);
                }catch (Exception e){
                    throw new RuntimeException(e);
                }finally {
                    unLock(lockKey);
                }
            });
        }
        //③获取锁失败，返回旧数据
        return r;
    }

    private boolean tryLock(String key){
        Boolean lock = redisTemplate.opsForValue().setIfAbsent(key, "lock", 10, TimeUnit.SECONDS);
        return BooleanUtil.isTrue(lock);
    }

    private void unLock(String key){
        redisTemplate.delete(key);
    }

}
